<?php


class Page extends ActiveRecord {
	
	
	public $path;
	public $depth;
	public $offline;
	public $tags;
	public $modified;
	public $created;
	
	public $revisions;
	
	
	
	/**
	 * Load all the contents associated with this page
	 * 
	 * We load all contents directly associated with the page as well as those
	 * "inherited" from previous revisions or parent pages.  The content pieces
	 * trickle down from the top page node (the home page) and are only ever
	 * overwritten for page specific content.  This allows a much more
	 * maintainable content management system.
	 * 
	 * $revision_id will be used if it is present, then $date will be used, then
	 * the current time if niether are present.
	 * 
	 * @return array An array of content objects
	 */
	public function loadContents() {
		
		$this->contents = Content::findBySql("SELECT *
			FROM
				(SELECT revisions.page_id, parent.name as pageName, revisions.number as revNumber,
					contents.*
				FROM
					pages as page
					INNER JOIN pages as parent ON page.lft BETWEEN parent.lft AND parent.rgt
					INNER JOIN revisions ON parent.id = revisions.page_id
					INNER JOIN contents ON revisions.id = contents.revision_id
				WHERE
					revisions.publish <= NOW() AND
					(revisions.expires IS NULL OR revisions.expires >= NOW())
					AND page.id = ?
				ORDER BY parent.lft DESC, revisions.publish DESC, contents.created DESC) AS tbl
			GROUP BY field", array($this->id));
	}
	
	/**
	 * Load all the contents associated with this page at a certain revision
	 * 
	 * We load all contents directly associated with the page as well as those
	 * "inherited" from previous revisions or parent pages.  The content pieces
	 * trickle down from the top page node (the home page) and are only ever
	 * overwritten for page specific content.  This allows a much more
	 * maintainable content management system.
	 * 
	 * $revision_id will be used if it is present, then $date will be used, then
	 * the current time if niether are present.
	 * 
	 * @return array An array of content objects
	 */
	public function loadContentsByRevision($revisionId) {
		$this->contents = Content::findBySql("SELECT *
			FROM
				(SELECT revisions.page_id, parent.name as pageName, revisions.number as revNumber,
					contents.*
				FROM
					pages as page
					INNER JOIN pages as parent ON page.lft BETWEEN parent.lft AND parent.rgt
					INNER JOIN revisions ON parent.id = revisions.page_id
					INNER JOIN contents ON revisions.id = contents.revision_id,
					revisions as thisRevision
				WHERE
					thisRevision.id = ?
					AND
					(revisions.id = thisRevision.id
					OR (revisions.publish <= IF(thisRevision.publish IS NOT NULL, thisRevision.publish, NOW())
						AND (revisions.expires IS NULL
							OR revisions.expires >= IF(thisRevision.publish IS NOT NULL, thisRevision.publish, NOW()))
					))
					AND page.id = ?
				ORDER BY parent.lft DESC, revisions.publish DESC) AS tbl
			GROUP BY field", array($revisionId, $this->id));
	}
	
	/**
	 * Load all the contents associated with this page at a certain date
	 * 
	 * We load all contents directly associated with the page as well as those
	 * "inherited" from previous revisions or parent pages.  The content pieces
	 * trickle down from the top page node (the home page) and are only ever
	 * overwritten for page specific content.  This allows a much more
	 * maintainable content management system.
	 * 
	 * $revisionId will be used if it is present, then $date will be used, then
	 * the current time if niether are present.
	 * 
	 * @return array An array of content objects
	 */
	public function loadContentsByDate($date) {
		$this->contents = PageContent::findBySql("SELECT contents.*
			FROM
				(SELECT revisions.page_id, parent.name as pageName, revisions.number as revNumber,
					contents.*
				FROM
					pages as page
					INNER JOIN pages as parent ON page.lft BETWEEN parent.lft AND parent.rgt
					INNER JOIN revisions ON parent.id = revisions.page_id
					INNER JOIN contents ON revisions.id = contents.revision_id
				WHERE
					revisions.publish <= ? AND
					(revisions.expires IS NULL OR revisions.expires >= ?)
					AND page.id = 1
				ORDER BY parent.lft DESC, revisions.publish DESC) AS tbl
			GROUP BY field");
	}
	
	public function getRevision($date = null, $revisionId = null) {
		if (!$this->revisions)
			$this->loadRevisions();
		
		if (is_numeric($revisionId)) {
			foreach ($this->revisions as $revision) {
				if ($revision->id == $revisionId) {
					$this->revision = $revision;
					break;
				}
			}// not found, throw error
		} else {
			if (!$date)
				$date = time();
			for ($i = 0; $i < count($this->revisions); $i++) {
				$revision = $this->revisions[$i];
				if (!$revision->publish) break;
				if ($revision->publish < $date && (!$revision->expires || $revision->expires > $date)) {
					$this->revision = $revision;
				}
			}
			if (!$this->revision && $revisionId)
				$this->revision = $this->revisions[$i ? $i - 1 : 0];
		}
		return $this->revision;
	}
	
	public function getRevisionByDate($date) {
		if ($this->revisions && $this->revision) {
			return $this->revision;
		}
		
		$this->loadRevisions();
		if (is_numeric($revisionId)) {
			foreach ($this->revisions as $revision) {
				if ($revision->id == $revisionId) {
					$this->revision = $revision;
					break;
				}
			}// not found, throw error
		} else {
			if (!$date)
				$date = time();
			for ($i = 0; $i < count($this->revisions); $i++) {
				$revision = $this->revisions[$i];
				if (!$revision->publish) break;
				if ($revision->publish < $date && (!$revision->expires || $revision->expires > $date)) {
					$this->revision = $revision;
				}
			}
			if (!$this->revision && $revisionId)
				$this->revision = $this->revisions[$i ? $i - 1 : 0];
		}
		return $this->revision;
	}
	
	
	function getLastRevisionNumber() {
		$max = 0;
		foreach ($this->revisions as $revision) {
			$max = max($max, $revision->number);
		}
		return $max;
	}
	
	/**
	 * Returns an array of the fields available in the template of the current
	 * revision.  Call loadRevisions first if a custom date/revisionId is
	 * needed, otherwise we it will use the current date as the basis to get
	 * the current revision and it's template.
	 *
	 */
	public function getTemplateFields($template) {
		if (!$this->revision)
			$this->loadRevisions();
		
		if (!$template || !file_exists($template))
			return array();
		
		
		$html = file_get_contents($dir . $template);
		preg_match_all('/(?:cms|cascade)(\w*)="\$?([\w_]+)[^"]*"/', $html, $matches);
		
		$fields = array();
		for ($i = 0; $i < count($matches[0]); $i++) {
			$fields[$matches[2][$i]] = '';
		}
		return $fields;
	}
	
	public static function savePage(Page $page, $parentId = null, $prevPageId = null) {
		$db = ActiveRecord::$db;
		$db->query("LOCK TABLES pages WRITE");
		
		list($lft, $depth, $path) = self::getLft($page, $parentId, $prevPageId);
		
		$db->query("UPDATE pages SET lft = lft + 2 WHERE lft >= $lft");
		$db->query("UPDATE pages SET rgt = rgt + 2 WHERE rgt >= $lft");
		$page->lft = $lft;
		$page->rgt = $lft + 1;
		$page->depth = $depth;
		$page->path = $path . $page->name;
		$page->save();
		
		$db->query("UNLOCK TABLES");
		
		$page->addRevision(new Revision());
		
		return $page;
	}
	
	public static function move(Page $page, $parentId = null, $prevPageId = null) {
		$db = ActiveRecord::$db;
		$db->query("LOCK TABLES pages WRITE");
		
		list($lft, $depth, $path) = self::getLft($page, $parentId, $prevPageId);
		
		$stmt = $db->prepare("SELECT id FROM pages WHERE lft >= ? AND rgt <= ?");
		$stmt->execute(array($page->lft, $page->rgt));
		$ids = array();
		foreach ($stmt as $row) {
			$ids[] = $row[0];
		}
		$ids = join(",", $ids);
		
		$distance = $page->rgt - $page->lft + 1;
		if ($lft > $page->lft)
			$lft -= $distance;
		$rgt = $lft + $distance - 1;
		
		$deltaLft = $lft - $page->lft;
		$deltaDepth = $depth - $page->depth;
		
		$trimPathLength = strlen(preg_replace('/\/?[^\/]+$/', "", $page->path));
		if ($trimPathLength) $trimPathLength++;
		
		if ($lft < $page->lft) {
			$stmt = $db->prepare("UPDATE pages SET lft = lft + ? WHERE lft >= ? AND lft < ?");
			$stmt->execute(array($distance, $lft, $page->lft));
			$stmt = $db->prepare("UPDATE pages SET rgt = rgt + ? WHERE rgt >= ? AND rgt < ?");
			$stmt->execute(array($distance, $lft, $page->lft));
		} else {
			$stmt = $db->prepare("UPDATE pages SET lft = lft - ? WHERE lft > ? AND lft <= ?");
			$stmt->execute(array($distance, $page->rgt, $rgt));
			$stmt = $db->prepare("UPDATE pages SET rgt = rgt - ? WHERE rgt > ? AND rgt <= ?");
			$stmt->execute(array($distance, $page->rgt, $rgt));
		}
		
		$stmt = $db->prepare("UPDATE pages SET depth = depth + ?, lft = lft + ?, rgt = rgt + ?,
			path = CONCAT(?, SUBSTR(path, ?))
		WHERE id IN ($ids)");
		$stmt->execute(array($deltaDepth, $deltaLft, $deltaLft, $path, $trimPathLength + 1));
		
		$page->lft = $lft;
		$page->rgt = $lft + $distance - 1;
		$page->path = $path . substr($page->path, $trimPathLength);
		
		$db->query("UNLOCK TABLES");
		
		return $page;
	}
	
	public static function renamePage(Page $page, $newName) {
		$db = ActiveRecord::$db;
		
		$newPath = preg_replace('/[^\/]+$/', $newName, $page->path);
		
		$stmt = $db->prepare("UPDATE pages SET path = CONCAT(?, SUBSTR(path, ?)) WHERE lft >= ? AND rgt <= ?");
		$stmt->execute(array($newPath, strlen($page->path) + 1, $page->lft, $page->rgt));
		
		if ($stmt->rowCount() == 0)
			return $page;
		
		Page::update($page->id, array("name = ?", $newName));
		
		$page->name = $newName;
		$page->path = $newPath;
		return $page;
	}
	
	public static function getLft(Page $page, $parentId = null, $prevPageId = null) {
		$db = ActiveRecord::$db;
		
		$lft = 0;
		$depth = 0;
		$path = "";
		
		if ($prevPageId) {
			$stmt = $db->prepare("SELECT rgt FROM pages WHERE id = ?");
			$stmt->execute(array($prevPageId));
			$row = $stmt->fetch();
			$lft = $row['rgt'] + 1;
		}
		
		if ($parentId) {
			$stmt = $db->prepare("SELECT lft, depth, path FROM pages WHERE id = ?");
			
			$stmt->execute(array($parentId));
			$row = $stmt->fetch();
			if ($lft == 0)
				$lft = $row['lft'] + 1;
			$depth = $row['depth'] + 1;
			$path = $row['path'] . "/";
		}
		
		if (!$prevPageId && !$parentId) {
			$lft = 1;
		}
		
		return array($lft, $depth, $path);
	}
	
	/**
	 * Delete a page (and subpages) from the hierarchy
	 *
	 * @param int $id
	 */
	public static function delete($id) {
		$db = ActiveRecord::$db;
		$db->query("LOCK TABLES
				pages p,
				revisions r,
				contents c,
				users_pages up,
				pages_tags pt,
				comments c2
			WRITE");
		
		$stmt = $db->prepare("SELECT lft, rgt FROM pages p WHERE id = ?");
		$stmt->execute(array($id));
		$result = $stmt->fetch(PDO::FETCH_OBJ);
		if (!$result || !$result->lft)
			return;
		
		$width = $result->rgt - $result->lft + 1;
		$db->query("DELETE FROM p, r, c, up, pt, c2
			USING
				pages p LEFT JOIN
				revisions r ON p.id = r.page_id LEFT JOIN
				contents c ON r.id = c.revision_id LEFT JOIN
				users_pages up ON p.id = up.page_id LEFT JOIN
				pages_tags pt ON p.id = pt.page_id LEFT JOIN
				comments c2 ON p.id = c2.page_id
			WHERE p.lft BETWEEN $result->lft AND $result->rgt");
		
		$db->query("UPDATE pages SET lft = lft - $width WHERE lft > $result->rgt");
		$db->query("UPDATE pages SET rgt = rgt - $width WHERE rgt > $result->rgt");
		
		$db->query("UNLOCK TABLES");
	}
	
	/**
	 * Find a page using a url path (i.e. /page/subpage/finalPage)
	 *
	 * @param string $path
	 * @return Page The page that was found, or null if none found
	 */
	static function findByPath($path) {
		$pages = Page::findBySql("SELECT * FROM page_view WHERE path = ?", array($path));
		if (count($pages) > 0)
			return $pages[0];
	}
	
	public function getPath() {
		if ($this->path)
			return $this->path;
		
		$db = DB::getInstance();
		$row = $db->getRow("SELECT GROUP_CONCAT(parent.name ORDER BY parent.lft SEPARATOR '/') AS path,
			page.*
			FROM pages AS page,
				pages AS parent
			WHERE page.lft BETWEEN parent.lft AND parent.rgt AND
				page.id = ?
		GROUP BY page.name
		ORDER BY page.lft", $this->id);
		if (!$row)
			return;
		return $row["path"];
	}
}

?>
